﻿using System;
using System.Collections.Generic;
using System.Security;
using System.Threading;
using MAF.UserSecurity.Repository;
using MAF.Common;

namespace MAF.UserSecurity
{
    /// <summary>
    /// 认证员，这是个抽象类，为了方便使用，使用了静态的方法管理不同的子类
    /// </summary>
    public abstract class Authenticator
    {

        #region 静态构造

        /// <summary>
        /// 静态构造方法
        /// </summary>
        static Authenticator()
        {
            ActiveUsers = new ActiveUserCollection();
        }

        #endregion

        #region 私有化单实例

        /// <summary>
        /// 构造方法，
        /// 为了方便使用本类，本来应该是静态类的，但是由于无法确定客户端的行为，所以要作为抽象类。
        /// </summary>
        protected Authenticator() { }

        /// <summary>
        /// 根据配置创建子类
        /// </summary>
        private static Authenticator _this
        {
            get
            {
                return new WebAuthenticator();  // TODO:目前只支持Web客户端，支持更多的客户端
            }
        }

        #endregion

        #region 公开
        
        /// <summary>
        /// 活动用户
        /// </summary>
        public static ActiveUserCollection ActiveUsers { get; private set; }

        /// <summary>
        /// 登录(认证)
        /// </summary>
        public static void Login(string name, string psw, DateTime expired)
        {
            // 判断当前用户是否已经登录
            if (UserContext.Current.IsLogon)
            {
                throw new SecurityException("您已经登录，无需再登录");
            }

            // 验证用户
            User user = _this.Authenticate(name, psw);

            // 清理过期的登录
            _this.ClearExpiredLogon(UserContext.Current.User.Id);

            // 创建用户登录上下文
            UserContext context = _this.CreateUserContext(user, expired);

            // 服务器端处理
            _this.LogUserOnServer(context);

            // 客户端处理
            _this.LogUserOnClient(context,expired);
        }

        /// <summary>
        /// 退出
        /// </summary>
        public static void Logout()
        {
            // 判断当前用户是否已经退出
            if (!UserContext.Current.IsLogon)
            {
                throw new SecurityException("您已经退出，无需再退出");
            }

            // 注销服务器端登录
            _this.LogUserOutServer(UserContext.Current.Token);

            // 注销客户端登录
            _this.LogUserOutClient(UserContext.Current.Token);
        }

        /// <summary>
        /// 保持登录状态。
        /// -- 本地访问不需要此方法，在登录信息在登录时就已经在主线程中记录。
        /// -- 远程访问时，每个连接都要提供登录是写在客户端的安全ID，并执行此方法才能维持登录状态。
        ///    例如：Web系统应该在Global.asax的Application_AuthenticateRequest 事件中调用此方法
        /// </summary>
        /// <param name="token"></param>
        public static void KeepLogon(Guid token)
        {
            _this.KeepUserLogOnClient(token);
        }

        #endregion

        #region 私有方法

        /// <summary>
        /// 验证用户名密码
        /// </summary>
        /// <param name="name">用户名</param>
        /// <param name="psw">密码</param>
        /// <returns>验证通过返回User对象，否则抛出异常</returns>
        private User Authenticate(string name, string psw)
        {
            // 获取用户
            Query<User> q = new Query<User>();
            q.AddCriterion("Name", Op.Equals, name);
            IList<User> users = _UserRepo.GetByQuery(q);

            var len = users.Count;
            if (1 < len)
                throw new Exception("系统错误,多个用户使用同一个登录名");

            if (1 > len)
                throw new Exception("用户不存在，请检查您输入的用户名是否有误");

            User user = users[0];

            // 验证密码
            if (user.Password != psw)
                throw new Exception("密码错误");

            // 返回通过验证的用户
            user.IsAuthenticated = true;
            return user;
        }

        /// <summary>
        /// 服务器端清理过期的登录上下文
        /// </summary>
        /// <param name="uid"></param>
        private void ClearExpiredLogon(Guid userId)
        {
            // 查询条件
            Query<UserContext> q = new Query<UserContext>();
            q.AddCriterion("User.Id", Op.Equals, userId);
            q.AddCriterion("Expired", Op.LessThan, DateTime.Now);

            // 清理
            _UserContextRepo.RemoveByQuery(q);
        }

        // 创建登录上下文
        private UserContext CreateUserContext(User user, DateTime expired)
        {
            // 创建UserContext对象
            return new UserContext(user, expired);
        }

        /// <summary>
        /// 服务器端记录登录上下文
        /// </summary>
        /// <param name="context"></param>
        private void LogUserOnServer(UserContext context)
        {
            // 把UserContext对象赋值给线程安全属性
            //Thread.CurrentPrincipal = context; // 下面的实现方法应该比这行更加稳妥，具体原理还不是很懂

            // 给程序集设置线程安全上下文
            AppDomain.CurrentDomain.SetThreadPrincipal(context);

            // 缓存UserContext对象(用Token作为映射键值)
            ActiveUsers.Add(context.Token, context);

            // 持久化UserContext对象
            _UserContextRepo.Add(context);
        }

        /// <summary>
        /// 服务器端注销登录上下文
        /// </summary>
        /// <param name="context"></param>
        private void LogUserOutServer(Guid token)
        {
            // 把UserContext对象赋值给线程安全属性
            Thread.CurrentPrincipal = null;

            // 缓存UserContext对象(用Token作为映射键值)
            ActiveUsers.Remove(token);

            // 持久化UserContext对象
            _UserContextRepo.Remove(token);
        }

        #endregion

        #region 抽象方法

        /// <summary>
        /// 客户端记录登录上下文
        /// </summary>
        /// <param name="isKeepLogon"></param>
        /// <param name="gotoHere"></param>
        protected abstract void LogUserOnClient(UserContext context, DateTime expired);

        /// <summary>
        /// 客户端注销登录上下文
        /// </summary>
        /// <param name="token"></param>
        protected abstract void LogUserOutClient(Guid token);

        /// <summary>
        /// 保持客户端登陆
        /// </summary>
        /// <param name="token"></param>
        protected virtual void KeepUserLogOnClient(Guid token)
        {
            // 设置程序集的安全主体为用户登录上下文
            AppDomain.CurrentDomain.SetThreadPrincipal(ActiveUsers[token]);
        }

        #endregion

        #region 私有属性
        
        /// <summary>
        /// 用户持久对象
        /// </summary>
        private UserRepo _UserRepo
        {
            get { return Configurator.UserRepo; }
        }

        /// <summary>
        /// 用户上下文持久对象
        /// </summary>
        private UserContextRepo _UserContextRepo
        {
            get { return Configurator.UserContextRepo; }
        }

        #endregion
    }
}